/****************************************************************************
**
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** You may use this file under the terms of the BSD license as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
**     of its contributors may be used to endorse or promote products derived
**     from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtCore/qendian.h>
#include <QDebug>
#include "utils.h"
#include "wavfile.h"
#include <math.h>
#include <QAudioOutput>

struct chunk
{
    char        id[4];
    quint32     size;
};

struct RIFFHeader
{
    chunk       descriptor;     // "RIFF"
    char        type[4];        // "WAVE"
};

struct WAVEHeader
{
    chunk       descriptor;
    quint16     audioFormat;
    quint16     numChannels;
    quint32     sampleRate;
    quint32     byteRate;
    quint16     blockAlign;
    quint16     bitsPerSample;
};

struct DATAHeader
{
    chunk       descriptor;
};

struct CombinedHeader
{
    RIFFHeader  riff;
    WAVEHeader  wave;
};

WavFile::WavFile(QObject *parent)
    : QObject(parent),
      m_waveDevice(NULL),
      m_headerLength(0),
      m_audioOutput(NULL),
      m_headerReadCommited(false),
      m_peakThreshold(0),
      m_isPlaying(false),
      m_audioBuffer(NULL)
{

}

WavFile::~WavFile()
{
    stop();
}

void WavFile::setWaveDevice(QIODevice *waveDevice)
{
    if (m_waveDevice != NULL)
    {
        disconnect(m_waveDevice, SIGNAL(readyRead()), this, SLOT(onDataReady()));
        m_waveDevice->deleteLater();
    }

    m_waveDevice = waveDevice;
    m_headerReadCommited = false;

    if (m_waveDevice != NULL)
    {
        connect(m_waveDevice, SIGNAL(readyRead()), this, SLOT(onDataReady()));

        if (!readHeader())
        {
            qDebug() << "Failed to read header!";
        }
        else
        {
            m_headerReadCommited = true;
        }
    }
}

const QAudioFormat &WavFile::fileFormat() const
{
    return m_fileFormat;
}

qint64 WavFile::headerLength() const
{
    return m_headerLength;
}

bool WavFile::readHeader()
{
    CombinedHeader header;
    bool result = m_waveDevice->read(reinterpret_cast<char *>(&header), sizeof(CombinedHeader)) == sizeof(CombinedHeader);
    if (result) {
        m_header.append(reinterpret_cast<char *>(&header), sizeof(CombinedHeader));
        if ((memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0
            || memcmp(&header.riff.descriptor.id, "RIFX", 4) == 0)
            && memcmp(&header.riff.type, "WAVE", 4) == 0
            && memcmp(&header.wave.descriptor.id, "fmt ", 4) == 0
            && (header.wave.audioFormat == 1 || header.wave.audioFormat == 0)) {

            // Read off remaining header information
            DATAHeader dataHeader;

            if (qFromLittleEndian<quint32>(header.wave.descriptor.size) > sizeof(WAVEHeader)) {
                // Extended data available
                quint16 extraFormatBytes;
                if (m_waveDevice->peek((char*)&extraFormatBytes, sizeof(quint16)) != sizeof(quint16))
                    return false;
                const qint64 throwAwayBytes = sizeof(quint16) + qFromLittleEndian<quint16>(extraFormatBytes);
                if (m_waveDevice->read(throwAwayBytes).size() != throwAwayBytes)
                    return false;
            }

            if (m_waveDevice->read((char*)&dataHeader, sizeof(DATAHeader)) != sizeof(DATAHeader))
                return false;

            // Establish format
            if (memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0)
                m_fileFormat.setByteOrder(QAudioFormat::LittleEndian);
            else
                m_fileFormat.setByteOrder(QAudioFormat::BigEndian);

            int bps = qFromLittleEndian<quint16>(header.wave.bitsPerSample);
            m_fileFormat.setChannels(qFromLittleEndian<quint16>(header.wave.numChannels));
            m_fileFormat.setCodec("audio/pcm");
            m_fileFormat.setFrequency(qFromLittleEndian<quint32>(header.wave.sampleRate));
            m_fileFormat.setSampleSize(qFromLittleEndian<quint16>(header.wave.bitsPerSample));
            m_fileFormat.setSampleType(bps == 8 ? QAudioFormat::UnSignedInt : QAudioFormat::SignedInt);

            if (m_isPlaying)
            {
                // if the wave device changed during play - continue to play
                play();
            }
        } else {
            result = false;
        }
    }
    m_headerLength = m_waveDevice->pos();
    return result;
}

void WavFile::onDataReady()
{
    if (m_headerReadCommited == false)
    {
        if (!readHeader())
        {
            emit error("Failed to read header!");
        }
        else
        {
            m_headerReadCommited = true;
        }
    }
    else
    {
        static const int ExpectedSamplesCount = 8192;
        static const int SampleSize = sizeof (qint16);
        qint16 arr[ExpectedSamplesCount] = {0};
        qint64 receivedBytes = m_waveDevice->read((char*) arr, ExpectedSamplesCount * SampleSize);
        qint64 samplesCount = receivedBytes / SampleSize;
        qreal sum = 0.0;

        if (m_isPlaying)
        {
            m_temporaryBuffer.append((char*) arr, receivedBytes);

            if (m_temporaryBuffer.length() >= m_audioOutput->periodSize())
            {
                m_audioBuffer->write(m_temporaryBuffer);
                m_temporaryBuffer.clear();
            }
            //qDebug() << receivedBytes << ":" << m_audioOutput->periodSize() << ":" << m_temporaryBuffer.pos() << ":" << m_temporaryBuffer.bytesAvailable();
        }
        for (int i = 0; i < samplesCount; i++)
        {
            qreal sample = pcmToReal(arr[i]);
            sum += sample * sample;
        }

        qreal rmsLevel = sqrt(sum / samplesCount);

        rmsLevel = qMax(qreal(0.0), rmsLevel);
        rmsLevel = qMin(qreal(1.0), rmsLevel);

        rmsLevel *= 10000;

        emit rmsLevelChanged(rmsLevel);

        if (rmsLevel > m_peakThreshold)
        {
            emit peakDetected();
            return;
        }
    }
}

void WavFile::onPeakThresholdChanged(int threshold)
{
    m_peakThreshold = threshold;
}

void WavFile::onVolumeChanged(int volume)
{
    if (m_audioOutput == NULL)
    {
        // TODO
    }
}

void WavFile::play()
{
    if (m_audioOutput == NULL)
    {
        m_audioOutput = new QAudioOutput(m_fileFormat);
        m_audioBuffer = m_audioOutput->start();
        m_isPlaying = true;
    }
}

void WavFile::stop()
{
    if (m_audioOutput != NULL)
    {
        m_audioOutput->stop();
        m_audioOutput->deleteLater();
        m_audioOutput = NULL;
        m_audioBuffer = NULL;
        m_isPlaying = false;
    }
}
